const ws = require('ws');
const fs = require('fs');
const ch = require("child_process");
const os = require("os")
const AK_FILE = __dirname + '/ak.json';

/**
 * 尝试一次连接至给定的服务器
 * @param {WSClient} clnt 客户端
 */
function connectOnce(clnt) {
    if(clnt.ws != null && clnt.ws.readyState === ws.OPEN) {
        return true;
    }
    console.log("正在连接服务器:", clnt.server);
    try{
        clnt.ws = new ws.WebSocket(clnt.server,{rejectUnauthorized: false});
    }catch{
        console.error("连接失败!")
        return false;
    }
    clnt.ws.on('open', () => {
        console.log('连接成功!');
        clnt.ocb(clnt.ws);
    })
    clnt.ws.on('message', clnt.mcb);
    clnt.ws.on("error", (err) => {
        console.log("连接错误:",err);
    })
    clnt.ws.on('close', () => {
        clnt.ws = null;
        if(!clnt.manualdisconnect) {
            console.warn('与服务器', clnt.server, '的连接被动断开, 尝试重新连接……')
            clnt.connect();
        }else{
            console.log('与服务器', clnt.server, '的连接主动断开.')
        }
        clnt.manualdisconnect = false;
    })
    return false;
}

/**
 * 尝试执行一次, 如果失败且未达到次数就等待重试
 * @param {NoParamCallback} func
 */
function tryOnce(inst, func){
    let ret = func();
    inst.nextTrialT = -1;
    if(ret) {
        inst.trialCount = 0;
        if(inst.trialTO != null) clearTimeout(inst.trialTO);
        return;
    }
    inst.trialCount++;
    if(inst.trialCount < inst.maxTrial) {
        inst.trialTO = setTimeout(()=>tryOnce(inst,func), inst.trialInterval * 1000);
        inst.nextTrialT = Date.now() + inst.trialInterval * 1000;
    }
}

class RepeatTrial {
    /**
     * @callback NoParamCallback
     * @returns {boolean} 是否执行成功
     */
    /**
     * 初始化多次尝试机构
     * @param {NoParamCallback} func 执行函数
     * @param {number} maxTrial 最大尝试次数
     * @param {number} trialInterval 尝试间隔，单位: 秒
     */
    constructor(func, maxTrial, trialInterval) {
        this.func = func
        this.maxTrial = maxTrial
        this.trialInterval = trialInterval
        this.trialCount = 0
        this.nextTrialT = -1;
        this.trialTO = null;
    }
    
    startTrial(){
        this.trialCount = 0;
        this.nextTrialT = -1;
        if(this.trialTO!=null) clearTimeout(this.trialTO);
        tryOnce(this,this.func);
    }
    /**
     * 停止尝试执行
     */
    stopTrial() {
        if(this.trialTO!=null) {
            clearTimeout(this.trialTO);
            this.nextTrialT = -1;
        }
    }
    /**
     * 返回下次尝试的时间
     * @returns {number} 下次尝试时间, 如果不存在，则为-1
     */
    nextTrialTime() {
        return this.nextTrialT;
    }
}

/**
 * WebSocket连接客户端封装
 */
class WSClient{
    /**
     * @callback MessageCallback
     * @param {string} message
     * @returns {undefined} Nothing is returned
     */
    /**
     * @callback OpenCallback
     * @returns {undefined} Nothing is returned
     */
    /**
     * @callback ClearCallback
     * @returns {undefined} Nothing is returned
     */
    /**
     * 初始化客户端
     * @param {string} server 服务器地址, 必须以ws://或wss://开头
     */
    constructor(server){
        if(!server.startsWith("wss://") && !server.startsWith("ws://")) {
            throw new Error("无效的服务器地址, 必须以ws://或wss://开头")
        }
        /**@type {string} */
        this.server = server 
        /**@type {WebSocket} */
        this.ws = null;
        /**@type {MessageCallback} */
        this.mcb = null;
        /**@type {OpenCallback} */
        this.ocb = null;
        /**@type {ClearCallback} */
        this.ccb = null; 
        this.manualdisconnect = false;
        /**@type {RepeatTrial} */
        this.connecttrialer = new RepeatTrial(()=>connectOnce(this), 10, 60);
    }

    /**
     * 指示是否已经连接到服务器
     * @returns {boolean} true或者false
     */
    isConnected(){
        return this.ws!=null && this.ws.readyState === ws.OPEN;
    }

    /**
     * 向服务器发送数据
     * @param {*} data 要发送的数据
     * @returns {boolean} true表示已尝试发送, false表示未连接
     */
    send(data) {
        if(!this.isConnected()) return false;
        this.ws.send(JSON.stringify(data));
        return true;
    }

    /**
     * 断开与服务器的连接
     */
    disconnect() {
        this.ccb();
        if(this.ws != null && this.ws.readyState === ws.OPEN) {
            this.connecttrialer.stopTrial();
            this.ws.close();
        }else{
            this.ws = null;
        }
        this.manualdisconnect = true;
    }
    /**
     * 反复尝试连接至给定的服务器, 最多10次, 每次间隔60秒
     */
    connect() {
        this.connecttrialer.startTrial();
    }
}

/**
 * 处理接受的信息
 * @param {PusherClient} clnt 推流客户端
 * @param {string} message 信息
 */
function recvMessage(clnt, message) {
    const data = JSON.parse(message);
    if(data["code"]!=0){
        if(data["op"]==="push"){
            console.log("收到要求推流信号, 开始申请推流.")
            clnt.startPush();
        }else if(data["op"]==="stop"){
            console.log("收到要求终止推流信号, 正在终止.")
            clnt.stopPush();
        }else{
            console.error("错误的数据:", data)
        }
        return;
    }
    if(data["msg"]==="allow push") {
        console.log("收到推流批准信号, 准备推流……")
        if(clnt.pusher_proc!=null){
            console.warn("已经在推流, 将不会重复执行指令.")
            return;
        }
        const new_cmd = clnt.pusher_cmd.replace(/\${id}/g,clnt.id).replace(/\${key}/g,clnt.key);
        console.log("开始推流, 命令为:", new_cmd)
        clnt.pusher_proc = ch.exec(new_cmd).on('exit', (code, signal) => {
            clnt.pusher_proc = null;
            console.log(`推流结束, 退出代码: ${code}, 信号: ${signal}`);
        }).on('error',(err)=>{
            clnt.pusher_proc = null;
            console.log(`推流错误: ${err}`);
        });
        clnt.send({op:"push"});
    }else if(data["msg"]==="push recv") {
        console.log("服务器已经收到推流信号.");
    }else if(data["msg"]==="stop recv") {
        console.log("服务器已经收到推流终止信号")
    }else if(data["msg"]==="hb ok") {
        clnt.lasthb = Date.now();
        console.log("心跳回应.")
    }else{
        console.error("错误的数据:", data)
    }
}

/**
 * Heartbeat
 * @param {PusherClient} clnt 
 */
function clntHB(clnt, check) {
    if(check && Date.now() - clnt.lasthb > 30000) {
        clearInterval(clnt.int);
    }
    clnt.send({op:"hb"})
}

class PusherClient extends WSClient {
    /**
     * 从配置文件创建PusherClient
     * @param {string} config 配置文件路径
     */
    constructor(config) {
        let data = null;
        try {
            data = JSON.parse(fs.readFileSync(config));
        } catch {
            data = {};
        }
        let svr = "ws://127.0.0.1:8080"
        if("server" in data) svr = data["server"];
        super(svr)
        this.int = null
        this.lasthb = Date.now();
        super.ocb = ()=>{
            clntHB(this,false);
            this.int = setInterval(()=>clntHB(this,true), 30000);
        };
        super.mcb = (message) => {recvMessage(this,message);}
        super.ccb = ()=>{
            clearInterval(this.int);
        }
        /**@type {string} */
        this.pusher_cmd = "./pusher"
        if("pusher" in data) this.pusher_cmd = data["pusher"];
        /**@type {string} */
        this.id = "rabbit";
        if("id" in data) this.id = data["id"];
        /**@type {string} */
        this.key = "rabbitdefault";
        if("key" in data) this.key = data["key"];
        /**@type {?ChildProcess} */
        this.pusher_proc = null
        super.connect()
    }
    
    /**
     * 将配置保存到文件
     * @param {string} 文件路径
     * @returns {boolean} 是否保存成功
     */
    saveConfig(config=AK_FILE) {
        try {
            fs.writeFileSync(config, 
                JSON.stringify({
                    "server": this.server,
                    "pusher": this.pusher_cmd,
                    "id": this.id,
                    "key": this.key
                }));
            return true;
        } catch {
            return false;
        }
    }

    /**
     * 发送数据
     * @param {*} data 数据
     * @returns 是否已尝试发送数据
     */
    send(data) {
        data["id"] = this.id;
        data["key"] = this.key;
        return super.send(data);
    }

    /**
     * 向服务器申请开始推流
     */
    startPush() {
        if(this.pusher_proc!=null) return;
        console.log("申请推流许可.")
        this.send({op:"push_req"});
    }
    /**
     * 停止推流
     */
    stopPush(){
        this.send({op:"stop"});
        if(this.pusher_proc!=null){
            console.log("推流进程PID:", this.pusher_proc.pid);
            if(os.platform() === 'win32'){
                ch.exec('taskkill /pid ' + this.pusher_proc.pid + ' /T /F')
            }else{
                ch.exec('kill ' + (this.pusher_proc.pid+1)); //新进程在shell中启动，由于shell本身也是一个进程，因此两者pid相差1
                this.pusher_proc.kill('SIGKILL');
            }
        }
    }
    /**
     * 是否正在推流
     * @returns {boolean}
     */
    isPushing(){
        return this.pusher_proc != null;
    }
    
}

const instance = new PusherClient(AK_FILE);
module.exports = instance;